/*
 * Copyright 2012 Marsrutai.lt
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.service;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lt.marsrutai.mobile.android.model.Bookmark;
import lt.marsrutai.mobile.android.util.BeanUtils;
import lt.marsrutai.mobile.android.util.BeanUtils.BeanProperty;
import lt.marsrutai.mobile.android.util.ClassUtils;
import lt.marsrutai.mobile.android.util.StringUtils;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

public class BookmarkService extends SQLiteOpenHelper {

    private static final String TABLE_NAME = "Bookmarks";
    private static final int DATABASE_VERSION = 1;

    private static final String FIELD_ID = "id";
    private static final String FIELD_TYPE = "type";
    private static final String FIELD_SCOPE_FOLDER = "scopeFolder";
    private static final String FIELD_SCOPE_NAME = "scopeName";
    private static final String FIELD_STOP_ID = "stopId";
    private static final String FIELD_STOP_NAME = "stopName";
    private static final String FIELD_STOP_DIRECTION_NAME = "stopDirectionName";
    private static final String FIELD_TRANSPORT_ID = "transportId";
    private static final String FIELD_SCHEDULE_ID = "scheduleId";
    private static final String FIELD_SCHEDULE_NAME = "scheduleName";
    private static final String FIELD_SCHEDULE_TRACK_NAME = "scheduleTrackName";
    private static final String FIELD_TRACK_ID = "trackId";
    private static final String FIELD_TRACK_NAME = "trackName";

    private static final String TEXT_TYPE = "TEXT";
    private static final String INT_TYPE = "INTEGER";

    /**
     * <p>
     * Column definitions.
     * </p>
     * <p>
     * Each top level array entry defines one field. The format of the
     * definition is as follows:
     * </p>
     * <p>
     * <code>{"column name", "column type", "extra"}</code>.
     * <p>
     * <p>
     * E.g. <code>{"colName", "INT", "PRIMARY KEY"}</code>.
     * </p>
     */
    private static final String[][] COLUMNS = new String[][] { { FIELD_ID, INT_TYPE, "PRIMARY KEY AUTOINCREMENT" },
            { FIELD_TYPE, TEXT_TYPE }, { FIELD_SCOPE_FOLDER, TEXT_TYPE }, { FIELD_SCOPE_NAME, TEXT_TYPE }, { FIELD_STOP_ID, TEXT_TYPE },
            { FIELD_STOP_NAME, TEXT_TYPE }, { FIELD_STOP_DIRECTION_NAME, TEXT_TYPE }, { FIELD_TRANSPORT_ID, TEXT_TYPE },
            { FIELD_SCHEDULE_ID, TEXT_TYPE }, { FIELD_SCHEDULE_NAME, TEXT_TYPE }, { FIELD_SCHEDULE_TRACK_NAME, TEXT_TYPE },
            { FIELD_TRACK_ID, TEXT_TYPE }, { FIELD_TRACK_NAME, TEXT_TYPE } };

    public BookmarkService(Context context) {
        super(context, TABLE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(getCreateTableQuery());
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL(String.format("DROP TABLE IF EXISTS %s;", TABLE_NAME));
        db.execSQL(getCreateTableQuery());
    }

    /**
     * Builds CREATE TABLE query from column definitions ({@link #COLUMNS})
     * 
     * @return
     */
    protected String getCreateTableQuery() {
        List<String> fieldQueryParts = new ArrayList<String>();
        for (String[] fieldDefinition : COLUMNS) {
            fieldQueryParts.add(StringUtils.join(fieldDefinition, " "));
        }

        String fieldsQuery = StringUtils.join(fieldQueryParts, ", ");

        return String.format("CREATE TABLE %s (%s);", TABLE_NAME, fieldsQuery);
    }

    /**
     * Adds a bookmark.
     * 
     * @param bookmark
     * @return <code>true</code> is bookmark was added, <code>false</code>
     *         otherwise
     */
    public boolean addBookmark(Bookmark bookmark) {
        // Gets the data repository in write mode
        SQLiteDatabase db = getWritableDatabase();

        ContentValues values = getContentValues(bookmark);

        long rowId = db.insert(TABLE_NAME, null, values);

        if (rowId > -1) {
            bookmark.setId(rowId);
        }

        return rowId > -1;
    }

    public boolean removeBookmark(Bookmark bookmark) {
        Bookmark bookmarkToDelete = bookmark;

        if (bookmark.getId() == null) {
            Set<? extends Bookmark> bookmarks = getBookmarks(bookmark.getClass());

            for (Bookmark savedBookmark : bookmarks) {
                if (savedBookmark.equals(bookmark)) {
                    bookmarkToDelete = savedBookmark;
                    break;
                }
            }
        }

        return delete(bookmarkToDelete);
    }

    private boolean delete(Bookmark bookmark) {
        SQLiteDatabase db = getWritableDatabase();

        return db.delete(TABLE_NAME, FIELD_ID + "=?", new String[] { Long.toString(bookmark.getId()) }) == 1;
    }

    public boolean updateBookmark(Bookmark bookmark) {
        Bookmark bookmarkToUpdate = bookmark;

        if (bookmark.getId() == null) {
            Set<? extends Bookmark> bookmarks = getBookmarks(bookmark.getClass());

            for (Bookmark savedBookmark : bookmarks) {
                if (savedBookmark.equals(bookmark)) {
                    bookmarkToUpdate = savedBookmark;
                    break;
                }
            }
        }

        return update(bookmarkToUpdate);
    }

    private boolean update(Bookmark bookmark) {
        SQLiteDatabase db = getWritableDatabase();

        return db.update(TABLE_NAME, getContentValues(bookmark), FIELD_ID + "=?", new String[] { Long.toString(bookmark.getId()) }) == 1;
    }

    protected ContentValues getContentValues(Bookmark bookmark) {
        Set<String> columnNames = new HashSet<String>();

        for (String[] fieldDefinition : COLUMNS) {
            columnNames.add(fieldDefinition[0]);
        }

        // Create a new map of values, where column names are the keys
        ContentValues values = new ContentValues();

        values.put(FIELD_TYPE, bookmark.getClass().getSimpleName());
        for (BeanProperty property : BeanUtils.getProperties(bookmark.getClass())) {
            if (columnNames.contains(property.getName())) {
                storeProperty(values, bookmark, property);
            }
        }
        return values;
    }

    public boolean containsBookmark(Bookmark bookmark) {
        Set<? extends Bookmark> bookmarks = getBookmarks(bookmark.getClass());

        return bookmarks.contains(bookmark);
    }

    public <T extends Bookmark> Set<T> getBookmarks(Class<T> bookmarkClass) {
        SQLiteDatabase db = getReadableDatabase();

        Set<String> columnNames = new HashSet<String>();

        for (String[] fieldDefinition : COLUMNS) {
            columnNames.add(fieldDefinition[0]);
        }

        List<String> projection = new ArrayList<String>();
        List<BeanProperty> propertiesToSelect = new ArrayList<BeanProperty>();
        for (BeanProperty property : BeanUtils.getProperties(bookmarkClass)) {
            if (columnNames.contains(property.getName())) {
                propertiesToSelect.add(property);
                projection.add(property.getName());
            }
        }

        Cursor cursor = db.query(TABLE_NAME, projection.toArray(new String[projection.size()]), FIELD_TYPE + "=?",
                new String[] { bookmarkClass.getSimpleName() }, null, null, FIELD_ID + " DESC");
        Set<T> bookmarks = new HashSet<T>();

        try {
            while (cursor.moveToNext()) {
                T bookmark = ClassUtils.instantiateClass(bookmarkClass);

                // fill object with data
                for (BeanProperty beanProperty : propertiesToSelect) {
                    BeanUtils.setPropertyValue(bookmark, beanProperty, retrieveProperty(cursor, beanProperty));
                }

                bookmarks.add(bookmark);
            }
        }
        finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        return bookmarks;
    }

    protected void storeProperty(ContentValues values, Bookmark bookmark, BeanProperty property) {
        Object value = BeanUtils.getPropertyValue(bookmark, property);
        Class<? extends Object> propertyType = property.getType();

        if (byte.class.isAssignableFrom(propertyType) || Byte.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Byte) value);
        }
        else if (short.class.isAssignableFrom(propertyType) || Short.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Short) value);
        }
        else if (int.class.isAssignableFrom(propertyType) || Integer.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Integer) value);
        }
        else if (long.class.isAssignableFrom(propertyType) || Long.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Long) value);
        }
        else if (float.class.isAssignableFrom(propertyType) || Float.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Float) value);
        }
        else if (double.class.isAssignableFrom(propertyType) || Double.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Double) value);
        }
        else if (boolean.class.isAssignableFrom(propertyType) || Boolean.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), (Boolean) value);
        }
        else if (char.class.isAssignableFrom(propertyType) || Character.class.isAssignableFrom(propertyType)
                || String.class.isAssignableFrom(propertyType)) {
            values.put(property.getName(), value.toString());
        }
        else if (propertyType.isEnum()) {
            values.put(property.getName(), ((Enum<?>) value).name());
        }
        else {
            throw new IllegalArgumentException(String.format("Unsupported property type %s.", propertyType));
        }
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected Object retrieveProperty(Cursor cursor, BeanProperty beanProperty) {
        int columnIndex = cursor.getColumnIndex(beanProperty.getName());
        Class<?> propertyType = beanProperty.getType();

        if (cursor.isNull(columnIndex)) {
            return null;
        }
        else if (byte.class.isAssignableFrom(propertyType) || Byte.class.isAssignableFrom(propertyType)) {
            return new Byte((byte) cursor.getInt(columnIndex));
        }
        else if (short.class.isAssignableFrom(propertyType) || Short.class.isAssignableFrom(propertyType)) {
            return cursor.getShort(columnIndex);
        }
        else if (int.class.isAssignableFrom(propertyType) || Integer.class.isAssignableFrom(propertyType)) {
            return cursor.getInt(columnIndex);
        }
        else if (long.class.isAssignableFrom(propertyType) || Long.class.isAssignableFrom(propertyType)) {
            return cursor.getLong(columnIndex);
        }
        else if (float.class.isAssignableFrom(propertyType) || Float.class.isAssignableFrom(propertyType)) {
            return cursor.getFloat(columnIndex);
        }
        else if (double.class.isAssignableFrom(propertyType) || Double.class.isAssignableFrom(propertyType)) {
            return cursor.getDouble(columnIndex);
        }
        else if (boolean.class.isAssignableFrom(propertyType) || Boolean.class.isAssignableFrom(propertyType)) {
            return Boolean.valueOf(cursor.getInt(columnIndex) == 1);
        }
        else if (char.class.isAssignableFrom(propertyType) || Character.class.isAssignableFrom(propertyType)) {
            return cursor.getString(columnIndex).charAt(0);
        }
        else if (String.class.isAssignableFrom(propertyType)) {
            return cursor.getString(columnIndex);
        }
        else if (propertyType.isEnum()) {
            return Enum.valueOf((Class) propertyType, cursor.getString(columnIndex));
        }
        else {
            throw new IllegalArgumentException(String.format("Unsupported property type %s.", propertyType));
        }
    }
}
